import { defineAsyncComponent, defineComponent, h, mergeProps, resolveComponent, unref } from 'vue';
import { get, isEmpty, isEqual, cloneDeep } from 'lodash-es';
import { tryOnUnmounted } from '@vueuse/core';
import version from 'ant-design-vue/es/version/index.js';
import message from 'ant-design-vue/es/message/index.js';
import Form from 'ant-design-vue/es/form/Form.js';
import FormItem from 'ant-design-vue/es/form/FormItem.js';
import Row from 'ant-design-vue/es/row/index.js';
import Col from 'ant-design-vue/es/col/index.js';
import Button from 'ant-design-vue/es/button/index.js';

export const defaultColSpan = { xs: 24, sm: 24, md: 12, lg: 12, xl: 6, xxl: 6 };
export const defaultLabelColSpan = { xs: 24, sm: 24, md: 12, lg: 12, xl: 8, xxl: 8 };
export const defaultWrapperColSpan = { xs: 24, sm: 24, md: 12, lg: 12, xl: 16, xxl: 16 };
export const formButtonOption = { show: true, fixRight: null, justifyContent: null };

const CFormItem = cloneDeep(FormItem);
if (version && version.substr(0, 1) > 3) {
  CFormItem.mounted = function () {
    if (this.$attrs.style && typeof this.$attrs.style === 'object') {
      let style = Object.assign({}, this.$attrs.style, { padding: 0 });
      Object.keys(style).forEach((key) => {
        this.$el.style.setProperty(key, style[key]);
      });
    }
  };
}

function unrefDeep(data) {
  data = unref(data);
  if (data && typeof data === 'object') {
    let result = Array.isArray(data) ? [] : {};
    data = Object.keys(data).reduce((res, key) => {
      /* 这里不用递归,否则类似 WrapFile 会丢失响应式导致回显出问题 */
      res[key] = unref(data[key]);
      return res;
    }, result);
  }
  return data;
}

function useStyleTag(css, options = {}) {
  let { doc = document, id = 'style_tag_' + Date.now(), autoRemove = true } = Object.assign({}, options);
  let el = doc.getElementById(id);
  if (!el) {
    el = doc.createElement('style');
    el.id = id;
    doc.head.appendChild(el);
    if (autoRemove === true) {
      tryOnUnmounted(() => {
        doc.head.removeChild(el);
        css = options = doc = id = el = null;
      });
    }
  }
  if (el.textContent !== css) {
    el.textContent = css;
  }
}

function getComponentName(option) {
  let { component } = Object.assign({}, option);
  return typeof component === 'string' ? component : component ? component.name || '' : '';
}

function isVoid(v) {
  return typeof v === 'undefined' || v === null;
}

export function isInput(option) {
  let name = getComponentName(option).toLocaleLowerCase();
  return ['input', 'textarea', 'editor', 'auto-complete', 'autocomplete'].some((text) => name.indexOf(text) >= 0);
}

export function isSelect(option) {
  let name = getComponentName(option).toLocaleLowerCase();
  return ['select', 'picker', 'switch', 'radio', 'checkbox', 'menu', 'cascader', 'rate', 'slider', 'upload'].some(
    (text) => name.indexOf(text) >= 0
  );
}

export function isDoubleInput(option) {
  let name = getComponentName(option).toLocaleLowerCase();
  return ['range'].some((text) => name.indexOf(text) >= 0);
}

export function isComponent(comp) {
  return comp ? typeof comp.render === 'function' || typeof comp.setup === 'function' : false;
}

export function getComponent(component) {
  if (typeof component === 'string') {
    let res = resolveComponent(
      component.replace(/^[a-z]|[_-][a-z]/gi, ($0) => {
        return $0.replace(/_-/g, '').toLocaleUpperCase();
      })
    );
    return isComponent(res) ? res : component;
  }
  if (typeof component === 'function') {
    try {
      let res = component();
      if (res instanceof Promise) {
        return defineAsyncComponent(component);
      }
    } catch (e) {
      return null;
    }
    return null;
  }
  return isComponent(component) ? component : null;
}

export function getPlaceholder(option, noArray) {
  option = Object.assign({}, option);
  let attrs = Object.assign({}, option.attrs, option.props);
  if (attrs.placeholder) {
    return unref(attrs.placeholder);
  }
  let prefix = isInput(option) ? '请输入' : isSelect(option) ? '请选择' : '请完善';
  let placeholder = prefix + (unref(option.label) || '');
  return isDoubleInput(option) && noArray !== true ? [placeholder, placeholder] : placeholder;
}

export function getRulesOption(option) {
  if (typeof option !== 'object' || !option) return;
  if (typeof option.rules === 'object' && option.rules) {
    return option.rules;
  }
  if (option.required === true) {
    return { required: true, message: getPlaceholder(option, true) };
  }
}

export function getSpanOption() {
  let args = [].slice.apply(arguments);
  return args.reduce((res, item) => {
    if (/^\d+$/.test(item)) {
      item = { span: item };
    }
    if (item && /^\d+$/.test(item.span)) {
      item.span = parseInt(item.span);
    }
    if (!item || typeof item !== 'object') {
      return res;
    }
    return Object.assign({}, res, item);
  }, {});
}

export function getColSpan(labelCol, wrapperCol) {
  labelCol = getSpanOption(labelCol);
  wrapperCol = getSpanOption(wrapperCol);
  let lSpan = labelCol.span;
  let wSpan = wrapperCol.span;
  let lSValid = lSpan && 0 < lSpan && lSpan < 24;
  let wSValid = wSpan && 0 < wSpan && wSpan < 24;
  if (lSValid) {
    return { labelCol: { ...labelCol, span: lSpan }, wrapperCol: { ...wrapperCol, span: 24 - lSpan } };
  }
  if (wSValid) {
    return { labelCol: { ...labelCol, span: 24 - wSpan }, wrapperCol: { ...wrapperCol, span: wSpan } };
  }
  return { labelCol: labelCol, wrapperCol: wrapperCol };
}

export function getDataFromOptions(option) {
  option = Object.assign({ keyField: 'field' }, option);
  let { options, keyField, valueField, filter, resultType } = option;
  let defaultValue = resultType === 'array' ? [] : {};
  let handler = (res, key, value) => Object.assign(res, { [key]: value });
  if (resultType === 'array') handler = (res, key, value) => res.concat(value);
  if (typeof filter !== 'function') filter = (val, key, item) => true;

  function getter(options, defaultValue, keyField, valueField, filter, handler) {
    return options.reduce((res, item) => {
      if (!item || typeof item !== 'object') return res;
      if (item.children && item.children.length) {
        res = getter(item.children, res, keyField, valueField, filter, handler);
      }
      let key = get(item, keyField);
      let val = unref(get(item, valueField));
      if (!key || typeof key !== 'string' || !filter(val, key, item)) return res;
      return handler(res, key, val);
    }, defaultValue);
  }
  return getter(options, defaultValue, keyField, valueField, filter, handler);
}

export function formatValue(value, type) {
  if (typeof type === 'function') {
    return type(value);
  }
  if (typeof value === type) {
    return value;
  }
  let formatter = {
    array: (v) => (Array.isArray(v) ? v : typeof v === 'string' ? v.split(',') : [v]),
    string: (v) => (Array.isArray(v) ? v.filter((i) => !isVoid(i)).join(',') : JSON.stringify(v)),
    number: (v) => (v = parseFloat(v)) || (v === 0 ? 0 : v ? v : undefined),
    boolean: (v) => (v === '0' ? false : !!v),
  };
  let format = formatter[type];
  return typeof format === 'function' ? format(value) : value;
}

export function formatValues(values, typeOptions) {
  if (!values || typeof values !== 'object') return {};
  if (!typeOptions || typeof typeOptions !== 'object' || isEmpty(typeOptions)) return values;
  Object.keys(values).forEach((key) => {
    if (typeOptions[key]) {
      values[key] = formatValue(values[key], typeOptions[key]);
    }
  });
  return values;
}

export function toMultiFieldsData(modelData, fields) {
  if (!Array.isArray(fields) || typeof modelData !== 'object' || !modelData) {
    return {};
  }
  return fields.reduce((result, field) => {
    if (!field || typeof field !== 'string') return result;
    if (field.indexOf(',') < 0) {
      if (field in modelData) result[field] = get(modelData, field);
      return result;
    }
    let values = field.split(',').reduce((res, key, index) => {
      if (!key || typeof key !== 'string') return res;
      if (key in modelData) res[index] = get(modelData, key);
      return res;
    }, []);
    if (values.length === 0) {
      return result;
    }
    result[field] = !values.every(isVoid) ? values : [];
    return result;
  }, {});
}

export function toSingleFieldsData(formData) {
  if (typeof formData !== 'object' || !formData) {
    return {};
  }
  let fields = Object.keys(formData);
  return fields.reduce((res, field) => {
    if (!field || typeof field !== 'string') return res;
    let val = get(formData, field);
    if (field.indexOf(',') < 0) {
      res[field] = val;
    } else {
      let keys = field.split(',');
      val = typeof val === 'string' ? val.split(',') : Array.isArray(val) ? val : [val];
      keys.forEach((key, i) => (res[key] = val[i]));
    }
    return res;
  }, {});
}

export function pickValues(source, fields, separator) {
  if (!source || typeof source !== 'object' || !Array.isArray(fields)) {
    return {};
  }
  let reg = new RegExp(',', 'g');
  if (separator && typeof separator === 'string') {
    reg = new RegExp(separator, 'g');
  } else if (separator === null || separator === false) {
    reg = null;
  }
  function getter(source, fields, reg, result) {
    return fields.reduce((res, field) => {
      if (reg && reg.test(field)) {
        res = getter(source, field.split(reg), reg, res);
      }
      if (field in source) {
        res[field] = source[field];
      }
      return res;
    }, result);
  }
  return getter(source, fields, reg, {});
}

export function mergeFormData(formData, modelValue, fields, separator) {
  if (!Array.isArray(fields)) {
    fields = Object.keys(formData);
  }
  let formValues = pickValues(modelValue, fields, separator);
  return Object.keys(formValues).reduce((res, key) => {
    let val = get(formValues, key, get(formData, key));
    return Object.assign(res, { [key]: val });
  }, {});
}

export async function validateForm(formRef, fields) {
  try {
    let values = await formRef.validate(fields);
    return Promise.resolve(values);
  } catch (e) {
    let field = get(e, 'errorFields[0].name[0]');
    let msg = get(e, 'errorFields[0].errors[0]');
    if (field) {
      formRef.scrollToField(field);
    }
    if (msg) {
      message.error(msg);
    }
    return Promise.reject(false);
  }
}

export default defineComponent({
  name: 'AntdForm',
  emits: ['update:modelValue', 'change', 'submit'],
  /* 不限制 expose, 方便联动操作 */
  // expose: ['validate', 'submit', 'reset'],
  props: {
    /*
     * options: [
     *   {
     *     component: string/object/function,
     *     attrs: object,
     *     props: object,
     *     slots: object/function,
     *     rules: object/array,
     *     label: string,
     *     field: string,
     *     valueType: string/function,
     *     format: string/function,
     *     colSpan: string/number/object,
     *     labelCol: string/number/object,
     *     wrapperCol: string/number/object,
     *     itemSpan: string/number/object,
     *     visible: boolean/function,
     *     children: array,
     *     style: object,
     *     onSubmit: function,
     *   },...
     * ]
     *  */
    options: { type: Array, default: () => [] },
    showCount: { type: Number, default: 3 },
    modelValue: { type: Object, default: () => ({}) },
    gutter: { type: [Number, Array], default: [12, 12] },
    colSpan: { type: [Object, Number], default: () => defaultColSpan },
    labelCol: { type: [Object, Number], default: () => defaultLabelColSpan },
    wrapperCol: { type: [Object, Number], default: () => defaultWrapperColSpan },
    formButtons: { type: [Object, Boolean], default: () => formButtonOption },
    useEnterPress: { type: Boolean, default: false },
  },
  data() {
    return {
      initValues: {},
      formData: {},
      fold: true,
    };
  },
  computed: {
    columns() {
      if (!Array.isArray(this.options) || !this.options.length) return [];
      return this.options.reduce((res, item) => {
        if (item && typeof item === 'object') res.push(item);
        return res;
      }, []);
    },
    formBtnOpts() {
      let isOverCount = this.columns.length > this.showCount;
      let formButtons = Object.assign({}, this.formButtons);
      return Object.assign({}, formButtonOption, formButtons, {
        show: this.formButtons !== null && this.formButtons !== false,
        justifyContent: formButtons.justifyContent || (isOverCount ? 'flex-end' : 'flex-start'),
        fixRight: typeof formButtons.fixRight === 'boolean' ? formButtons.fixRight : isOverCount,
      });
    },
    hasArrow() {
      if (this.formBtnOpts.show === false) return false;
      let list = this.columns.filter((item) => this.getVisible(item));
      return list.length > this.showCount;
    },
    valueTypes() {
      return getDataFromOptions({
        options: this.columns,
        valueField: 'valueType',
        resultType: 'object',
        filter: (v) => !isVoid(v),
      });
    },
    valueFormats() {
      const data = getDataFromOptions({
        options: this.columns,
        valueField: 'format',
        resultType: 'object',
        filter: (v) => !isVoid(v),
      });
      return Object.keys(data).reduce((res, key) => {
        key.split(',').forEach((i) => (res[i] = data[key]));
        return res;
      }, {});
    },
    fields() {
      return getDataFromOptions({
        options: this.columns,
        valueField: 'field',
        resultType: 'array',
      });
    },
    values() {
      return getDataFromOptions({
        options: this.columns,
        valueField: 'value',
        resultType: 'object',
      });
    },
  },
  created() {
    useStyleTag(
      `
      .antd-form .antd-form-row .antd-form-col .antd-form-item {
        position: relative;
      }
      .antd-form .antd-form-row .antd-form-col .antd-form-item .ant-form-item-explain {
        position: absolute;
        top: 100%;
        left: 0;
        right: 0;
        margin: auto;
        font-size: 14px;
        text-align: right;
      }
      `,
      { id: 'antd_form_explain_error', autoRemove: false }
    );
    const formData = toMultiFieldsData(unrefDeep(this.modelValue), this.fields);
    this.initValues = formatValues(formData, this.valueTypes);
    this.formData = Object.assign({}, this.values, this.initValues);
    this.$watch(
      () => this.modelValue,
      (modelValue) => {
        let formData = toMultiFieldsData(unrefDeep(modelValue), this.fields);
        formData = formatValues(formData, this.valueTypes);
        if (!isEmpty(formData) && !isEqual(this.formData, formData)) {
          this.formData = Object.assign({}, this.formData, formData);
          this.$nextTick(() => this.$refs.formRef.clearValidate());
        }
      },
      { immediate: false, deep: true }
    );
  },
  methods: {
    getSpaces(format) {
      let gutter = this.gutter;
      if (typeof format !== 'function') format = (i) => i;
      if (!Array.isArray(gutter)) gutter = [gutter, gutter];
      return gutter.slice(0, 2).map((item) => format(Number.isFinite(item) ? item : 12));
    },
    getVisible(item) {
      let visible = typeof item.visible === 'function' ? item.visible() : item.visible;
      return unref(visible) !== false;
    },
    registerHooks(key, hook) {
      if (!this.hooks || typeof this.hooks !== 'object') {
        this.hooks = {};
      }
      if (key && typeof hook === 'function') {
        this.hooks[key] = hook;
      }
    },
    async runRegisterHooks(params) {
      if (this.hooks && typeof this.hooks === 'object') {
        await Promise.all(
          Object.keys(this.hooks).map(async (key) => {
            let hook = this.hooks[key];
            if (typeof hook === 'function') {
              let res = await hook({ key, params });
              if (res === false) return Promise.reject(false);
              if (!isVoid(res)) params[key] = res;
            }
          })
        );
      }
      return params;
    },
    async runOptionHooks(params) {
      let optionHooks = getDataFromOptions({
        options: this.columns,
        valueField: 'onSubmit',
        resultType: 'object',
      });
      await Promise.all(
        Object.keys(optionHooks).map(async (key) => {
          let hook = optionHooks[key];
          if (typeof hook === 'function') {
            let instance = this.$refs[key];
            let res = await hook({ key, params, instance });
            if (res === false) return Promise.reject(false);
            if (!isVoid(res)) params[key] = res;
          }
        })
      );
      return params;
    },
    async validate() {
      let fields = getDataFromOptions({
        options: this.columns,
        valueField: 'field',
        resultType: 'array',
        filter: (val, key, item) => this.getVisible(item),
      });
      return await validateForm(this.$refs.formRef, fields);
    },
    output(useEmit) {
      let modelValue = unrefDeep(this.modelValue);
      let values = toSingleFieldsData(this.formData);
      values = Object.assign({}, modelValue, values);
      if (useEmit !== false && !isEqual(values, modelValue)) {
        this.$emit('update:modelValue', values);
        this.$emit('change', values);
      }
      return values;
    },
    async submit(validate) {
      if (validate !== false) {
        await this.validate();
      }
      let values = this.output(false);
      values = await this.runRegisterHooks(values);
      values = await this.runOptionHooks(values);
      values = formatValues(values, this.valueFormats);
      this.$emit('submit', values);
      return values;
    },
    async reset() {
      this.formData = Object.assign({}, this.values, this.initValues);
      return await this.submit();
    },
    renderArrow() {
      let attrs = {
        class: 'antd-form-folder',
        style: { cursor: 'pointer', userSelect: 'none' },
        onClick: () => (this.fold = !this.fold),
      };
      return this.hasArrow ? h('span', attrs, this.fold === true ? '展开∨' : '收起∧') : null;
    },
    renderFormButtons() {
      let spaces = this.getSpaces((item) => item + 'px');
      let formItemStyle = {
        width: '100%',
        display: 'flex',
        flexWrap: 'nowrap',
        alignItems: 'center',
        justifyContent: this.formBtnOpts.justifyContent,
        gap: '12px',
        padding: '1.5px 0 1.5px 24px',
      };
      return h(
        CFormItem,
        {
          class: 'antd-form-item',
          style: { width: '100%', margin: 0, padding: spaces.join(' ') },
          labelCol: { span: 0 },
          wrapperCol: { span: 24 },
        },
        () =>
          h('div', { style: formItemStyle }, [
            h(Button, { type: 'primary', onClick: () => this.submit() }, () => '搜索'),
            h(Button, { type: 'default', onClick: () => this.reset() }, () => '重置'),
            this.renderArrow(),
          ])
      );
    },
    renderColHolder(colStyle) {
      let spaces = this.getSpaces((item) => item + 'px');
      return h(Col, { class: 'antd-form-col', style: colStyle, ...getSpanOption(this.colSpan) }, () =>
        h(CFormItem, { class: 'antd-form-item', style: { margin: 0, padding: spaces.join(' ') } })
      );
    },
    renderToggle(children) {
      if (this.formBtnOpts.show === false) {
        return null;
      }
      let colStyle = { boxSizing: 'border-box', display: 'flex', alignItems: 'center', padding: 0 };
      let fixRightStyle = { position: 'absolute', width: '100%', bottom: 0, right: 0 };
      let fixStyle = this.formBtnOpts.fixRight !== false ? fixRightStyle : {};
      let colOption = getSpanOption(this.colSpan);
      if (this.formBtnOpts.fixRight !== false) {
        children.push(this.renderColHolder(colStyle));
      }
      children.push(
        h(Col, { class: 'antd-form-col', style: { ...colStyle, ...fixStyle }, ...colOption }, () =>
          this.renderFormButtons()
        )
      );
    },
    renderComp(item) {
      let onUpdate = (value) => {
        this.formData = { ...this.formData, [item.field]: unref(value) };
        this.output(true);
        this.$nextTick(() => {
          this.$refs.formRef.clearValidate([item.field]);
        });
      };
      let attrs = { scope: item, formData: this.formData };
      let value = this.formData[item.field];
      let placeholder = getPlaceholder(item);
      let _props = mergeProps({ style: { width: '100%' }, placeholder }, item.attrs, item.props);
      let props = unrefDeep(
        Object.assign(_props, {
          ref: item.field,
          value: value,
          modelValue: value,
          'onUpdate:value': onUpdate,
          'onUpdate:modelValue': onUpdate,
          onRegisterSubmitHook: (hook) => this.registerHooks(item.field, hook),
          onVnodeUnmounted: () => (item = null),
        })
      );
      if (this.$slots[item.field]) {
        return this.$slots[item.field]({ ...attrs, props });
      }
      let component = getComponent(item.component);
      if (!component && typeof item.component === 'function') {
        return item.component({ ...attrs, props, h });
      }
      return component ? h(component, props, item.slots) : null;
    },
    renderItem(item) {
      if (item.children && item.children.length) {
        return h('div', { style: { flex: 'none', width: '100%', margin: 0 } }, [
          this.renderRow(item.children, { itemSpan: item.itemSpan }),
        ]);
      }
      let spaces = this.getSpaces((item) => item + 'px');
      let label = unref(item.label);
      let hasLabel = !isVoid(label);
      let { labelCol, wrapperCol } = getColSpan(
        getSpanOption(this.labelCol, item.labelCol),
        getSpanOption(this.wrapperCol, item.wrapperCol)
      );
      return h(
        CFormItem,
        {
          class: 'antd-form-item',
          style: { flex: 'none', width: '100%', margin: 0, padding: spaces.join(' ') },
          label: label,
          name: item.field,
          rules: getRulesOption(item),
          labelCol: !hasLabel ? 0 : labelCol,
          wrapperCol: !hasLabel ? 24 : wrapperCol,
          onVnodeUnmounted: () => (item = null),
        },
        () => this.renderComp(item)
      );
    },
    renderCol(item, options) {
      let { show, itemSpan } = Object.assign({}, options);
      let colSpan = getSpanOption(this.colSpan, itemSpan, item.colSpan);
      let colStyle = { boxSizing: 'border-box', display: 'flex', alignItems: 'center', padding: 0 };
      let style = Object.assign({}, colStyle, item.style);
      if (!this.getVisible(item) || show === false) {
        style.display = 'none';
      }
      let attrs = {
        ...colSpan,
        class: 'antd-form-col',
        key: item.field,
        style: style,
        onVnodeUnmounted: () => (item = null),
      };
      return h(Col, attrs, () => this.renderItem(item));
    },
    renderRow(columns, options) {
      if (!columns || !columns.length) {
        return null;
      }
      let { itemSpan, showCount, afterHook } = Object.assign({}, options);
      let style = { position: 'relative', width: 'unset', margin: 0 };
      let count = showCount || Infinity;
      let children = columns.reduce((res, item) => {
        let show = this.getVisible(item);
        if (show && this.hasArrow) {
          show = count-- > 0 ? true : !this.fold;
        }
        if (typeof item === 'object' && item) {
          res.push(this.renderCol(item, { show, itemSpan }));
        }
        return res;
      }, []);
      if (typeof afterHook === 'function') {
        afterHook(children);
      }
      let attrs = {
        class: 'antd-form-row',
        style: style,
        type: 'flex',
        gutter: 0,
        onVnodeUnmounted: () => (options = children = count = null),
      };
      return h(Row, attrs, () => children);
    },
  },
  render() {
    let { labelCol, wrapperCol } = getColSpan(
      getSpanOption(defaultLabelColSpan, this.labelCol),
      getSpanOption(defaultWrapperColSpan, this.wrapperCol)
    );
    return h(
      'div',
      { class: 'antd-form', style: { boxSizing: 'border-box', width: '100%' } },
      h(
        Form,
        {
          ref: 'formRef',
          style: { boxSizing: 'border-box', width: '100%' },
          model: this.formData,
          validateTrigger: 'blur',
          labelCol: labelCol,
          wrapperCol: wrapperCol,
          onKeyup: (e) => {
            // 新增、修改、表单里面不能使用这个
            this.useEnterPress === true && e && e.key === 'Enter' && this.submit();
          },
        },
        () =>
          this.renderRow(this.columns, {
            showCount: this.showCount,
            afterHook: this.renderToggle,
          })
      )
    );
  },
});
